/**
 * 
 */
package x.java.tea;

import java.io.IOException;
import java.io.InputStream;

/**
 * FilterInputStream contains some other input stream, which it uses as its
 * basic source of data, possibly transforming the data along the way.
 * 
 * The class itself overrides all methods of InputStream and delegates all
 * requests to the contained input stream after passing through a state machine.
 * If a marker is recognized, the injector will be invoked and the injection data
 * stream will be merged into the input stream.
 * 
 * @author qiangli
 * 
 */
public class FilterInputStream extends InputStream {
	protected InputStream in;

	protected Injector injector;

	private State state;

	private byte[] buf;

	private int cnt;

	private int pos;

	private boolean injecting;

	public FilterInputStream() {
		this(null, null, Constant.ID_PATTERN, Constant.MAX_ID_LEN,
				Constant.MAX_BUF_SIZE);
	}

	public FilterInputStream(InputStream in) {
		this(in, null, Constant.ID_PATTERN, Constant.MAX_ID_LEN,
				Constant.MAX_BUF_SIZE);
	}

	public FilterInputStream(InputStream in, Injector injector) {
		this(in, injector, Constant.ID_PATTERN, Constant.MAX_ID_LEN,
				Constant.MAX_BUF_SIZE);
	}

	public FilterInputStream(InputStream in, Injector injector,
			String idpattern, int maxidlen, int maxbufsize) {
		init(in, injector, idpattern, maxidlen, maxbufsize);
	}

	private void init(InputStream in, Injector injector, String idpattern,
			int maxidlen, int maxbufsize) {
		this.in = in;
		this.injector = injector;
		this.buf = new byte[maxbufsize];
		this.pos = 0;
		this.cnt = 0;
		this.state = new State(idpattern.getBytes(), new byte[maxidlen]);
		this.injecting = false;
	}

	public void setInputStream(InputStream in) {
		this.in = in;
	}

	public void setInjector(Injector injector) {
		this.injector = injector;
	}

	private int inject() throws IOException {
		pos = 0;
		while (true) {
			cnt = injector.inject(buf);
			if (cnt == -1) {
				injecting = false;
				return -1;
			}
			if (cnt == 0) {
				continue;
			}
			//
			pos = 1;
			return buf[0];
		}
	}

	@Override
	public int read() throws IOException {
		if (pos < cnt) {
			return buf[pos++];
		}
		if (injecting) {
			int b = inject();
			if (b > -1) {
				return b;
			}
		}
		return filter();
	}

	protected int filter() throws IOException {
		if (in == null) {
			return -1;
		}
		while (true) {
			int b = in.read();
			if (b == -1) {
				return -1;
			}
			int s = state.filter(b);
			switch (s) {
			case State.ABORT:
				buf[0] = (byte) b;
				pos = 0;
				cnt = 1;
				return state.getStart();
			case State.LEAVE:
				injecting = true;
				injector.init(new String(state.getId()));
				return inject();
			case State.NORMAL:
				pos = 0;
				cnt = 0;
				return b;
			case State.START:
			case State.ENTER:
			default:
				pos = 0;
				cnt = 0;
			}
		}
	}

	@Override
	public int available() throws IOException {
		return 0; // TODO
	}

	@Override
	public boolean markSupported() {
		return false; // TODO
	}

	@Override
	public int read(byte[] b, int off, int len) throws IOException {
		for (int i = 0; i < len; i++) {
			int c = read();
			if (c == -1) {
				return i > 0 ? i : -1;
			}
			b[off + i] = (byte) c;
		}
		return len;
	}

	@Override
	public int read(byte[] b) throws IOException {
		return read(b, 0, b.length);
	}
}
